package javango.contrib.hibernate;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import javango.db.ManagerException;

import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.cfg.Configuration;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Expression;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.SimpleExpression;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.Value;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.type.AssociationType;
import org.hibernate.type.CollectionType;
import org.hibernate.type.ManyToOneType;
import org.hibernate.type.Type;

public class HibernateQuerySetHelper {
	private final static Log log = LogFactory.getLog(HibernateQuerySetHelper.class);
	
	protected HibernateUtil hibernateUtil;
	protected Class<?> modelClass;
	protected Criteria criteria;
	Map<String, Criteria> joins = new HashMap<String, Criteria>();
	
	public HibernateQuerySetHelper(HibernateUtil hibernateUtil, Class modelClass, Criteria criteria) {
		this.hibernateUtil = hibernateUtil;
		this.modelClass = modelClass;
		this.criteria = criteria;
	}

	/**
	 * Returns a the keys Class[] 
	 * @return
	 * @throws DaoException
	 */
	public Class[] getPkClass() throws ManagerException {
		Configuration cfg = hibernateUtil.getConfiguration();
		PersistentClass pclass = cfg.getClassMapping(modelClass.getName());

		if (pclass == null) {
			throw new ManagerException("Unable to find class : "
					+ modelClass.toString());
		}

		Property componentProperty = pclass.getIdentifierProperty();
		if (componentProperty == null) {
			Component component = pclass.getIdentifierMapper();
			if (component == null) {
				throw new ManagerException("Unable to get pk mapping");
			}
			if (log.isDebugEnabled()) log.debug(String.format("Found %d keys for model %s", component.getPropertySpan(), modelClass.getName()));

			Class<?>[] classArray = new Class[component.getColumnSpan()];
			Iterator<Property> ite = component.getPropertyIterator();
			int i = 0;
			while (ite.hasNext()) {
				Property p = ite.next();
				if (log.isDebugEnabled()) log.debug("property name: " + p.getName());
				classArray[i++] = p.getType().getReturnedClass();
			}
			return classArray;
		} else {

			Value value = componentProperty.getValue();
			if (value == null)
				throw new ManagerException("Component value is null");
			else {
				if (log.isDebugEnabled()) log.debug(String.format("Found simple key for model %s '%s'",
						modelClass.getName(), value.getType()
								.getReturnedClass()));
				return new Class[] { value.getType().getReturnedClass() };
			}
		}
	}

	public java.lang.reflect.Field findField(Class modelClass, String property) throws ManagerException {
		// TODO dry,  this is repeated in ModelForm,  maybe helper/util class..
		Class classToTry = modelClass;
		while(classToTry != null) {
			try {
				return classToTry.getDeclaredField(property);
			} catch (NoSuchFieldException e) {
				classToTry = classToTry.getSuperclass();
			}
		}
		throw new ManagerException("No such field found: " + property + " in class " + modelClass.getName());
	}
	
	private ConvertUtilsBean convertUtilsBean = new ConvertUtilsBean();
	public Object convert(String value, Class clazz) {		
		return convertUtilsBean.convert(value, clazz);
	}
	
	public Object correctType(String fieldName, Object value) throws ManagerException {
		return correctType(fieldName, value, this.modelClass);
	}
	
	public Object correctType(String fieldName, Object value, Class clazz) throws ManagerException {
		
		// TODO May still need to convert non-strings to the correct value (ie long to double, etc).
		if (!(value instanceof String)) {
			return value;
		}		
		ClassMetadata metadata = hibernateUtil.getSessionFactory().getClassMetadata(clazz);
		if (metadata != null) {
			Type type = metadata.getPropertyType(fieldName);
			if (type instanceof CollectionType) {
				log.warn("Unable to handle collection: " + fieldName);
				return value;
			} else if (type instanceof AssociationType) {
				AssociationType assocType = (AssociationType)type;
				Class[] assocPkClass = getPkClass();
				if (assocPkClass.length > 1) {
					log.error("Multi keyed associations are not supported in correctType");
					return value;
				}
				Object o = convert((String)value, assocPkClass[0]);
				return hibernateUtil.getSession().get(modelClass, (Serializable)o);
				// TODO if return value is null should something be done??
				//return value;
			}				
		}
			
		java.lang.reflect.Field field = findField(clazz, fieldName);
		return convert((String)value, field.getType());

	}
	
	public Object convertToPk(Object pk) throws ManagerException {
	if (pk == null) return null;
		
		Class[] keyClass = getPkClass();
		if (keyClass.length != 1) {
			throw new ManagerException("get(pk) not supported for composite keyed models");
		}
				
		if (pk.getClass().equals(keyClass[0])) {
			return pk;
		}		
		
		if (log.isDebugEnabled()) log.debug("Trying to convert input to type " + keyClass[0]);
		
		return convert(pk.toString(), keyClass[0]);
	}

	public void resolveProperty() {
		
	}
	
	protected Criterion getCriterion(String searchType, String searchField, Object searchValue, boolean property) throws ManagerException {
		if ("like".equals(searchType)) {
			// the as400 in particular does not like likes without percents on char fields
			if (searchValue instanceof String && StringUtils.contains((String)searchValue, "%")) {
				return Expression.like(searchField, searchValue);
			}
			if (property) {
				return Expression.eqProperty(searchField, (String)searchValue);
			} else {
				return Expression.eq(searchField, searchValue);
			}
		} else if ("ilike".equals(searchType)) {
			if (searchValue instanceof String && StringUtils.contains((String)searchValue, "%")) {
				return Expression.ilike(searchField, searchValue);
			}
			return Expression.eq(searchField, searchValue).ignoreCase();
		} else if ("nlike".equals(searchType)) {
			// the as400 in particular does not like likes without percents on char fields
			if (searchValue instanceof String && StringUtils.contains((String)searchValue, "%")) {
				return Expression.not(Expression.like(searchField, searchValue));
			}
			return Expression.ne(searchField, searchValue);
		} else if ("nilike".equals(searchType)) {
			if (searchValue instanceof String && StringUtils.contains((String)searchValue, "%")) {
				return Expression.not(Expression.ilike(searchField, searchValue));
			}
			return Expression.ne(searchField, searchValue).ignoreCase();
		} else if ("lte".equals(searchType)) {
			if (property) {
				return Expression.leProperty(searchField, (String)searchValue);
			} else {
				return Expression.le(searchField, searchValue);
			}
		} else if ("gte".equals(searchType)) {
			if (property) {
				return Expression.geProperty(searchField, (String)searchValue);
			} else {
				return Expression.ge(searchField, searchValue);
			}
		} else if ("lt".equals(searchType)) {
			if (property) {
				return Expression.ltProperty(searchField, (String)searchValue);
			} else {
				return Expression.lt(searchField, searchValue);
			}
			
		} else if ("gt".equals(searchType)) {
			if (property){
				return Expression.gtProperty(searchField, (String)searchValue);
			} else {
				return Expression.gt(searchField, searchValue);
			}
		} else if ("eq".equals(searchType)) {
			if (searchValue == null) {
				return Expression.isNull(searchField);
			}
			if (property) {
				return Expression.eqProperty(searchField, (String)searchValue);
			} else {
				return Expression.eq(searchField, searchValue);
			}
		} else if ("ne".equals(searchType)) {
			if (searchValue == null) {
				return Expression.isNotNull(searchField);
			}
			if (property) {
				return Expression.neProperty(searchField, (String)searchValue);
			} else {
				return Expression.ne(searchField, searchValue);
			}
		} else if ("in".equals(searchType)) {
			return Expression.in(searchField, (Object[])searchValue);
		} else if ("notin".equals(searchType)) {
			return Expression.not(Expression.in(searchField, (Object[])searchValue));
		} else if ("date".equals(searchType)) {
			if (!(searchValue instanceof Date) || searchValue == null) {
				throw new ManagerException ("__date search requires a non-null instance of java.util.Date");
			}
			Date date = (Date)searchValue;
			
			Calendar from = new GregorianCalendar();
			from.setTime(date);
			from.set(Calendar.HOUR_OF_DAY, 0);
			from.set(Calendar.MINUTE, 0);
			from.set(Calendar.SECOND, 0);

			Calendar to = new GregorianCalendar();
			to.setTime(date);
			to.set(Calendar.HOUR_OF_DAY, 23);
			to.set(Calendar.MINUTE, 59);
			to.set(Calendar.SECOND, 59);
			
			return Expression.between(searchField, from.getTime(), to.getTime());
		}
		return null;
		
	}
	
	protected Object[] resolveJoins(String[] parts, int count) throws ManagerException {
		Criteria c = criteria; // start with the main criteria,  if we have joins this will eventually be the criteria for the join we want.
		int part = 0; // the part we are looking at.  process util we get through them all.
		Class searchClass = this.modelClass; 
		String joinPath = null; // this gets built to the full path for the joins map's key		
		if (count > 0) {
			// we got some joins..  resolve them			
			while (part < count) { // get all but the last two which may be a searchProperty and searchType
				String joinProperty = parts[part];
				joinPath = joinPath == null ? joinProperty : joinPath + "__" + joinProperty;
				Criteria subC = joins.get(joinPath);
				if (subC == null) {
					subC = c.createCriteria(joinProperty, Criteria.LEFT_JOIN);
					joins.put(joinPath, subC);
				}
				ClassMetadata metadata = hibernateUtil.getSessionFactory().getClassMetadata(searchClass);
				Type type = metadata.getPropertyType(joinProperty);
				if (type instanceof CollectionType) {
					log.warn("Expiremental CollectionType join: " + joinPath);
					subC.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
					Type newtype = ((CollectionType)type).getElementType((SessionFactoryImplementor)hibernateUtil.getSessionFactory());
					searchClass = newtype.getReturnedClass();
				} else {
					searchClass = type.getReturnedClass();
				}
				c = subC;
				part++;
				

			}
		}
		return new Object[]{searchClass, c};
	}
	
	public void processField(String fieldName, Object value, boolean property) throws ManagerException {		
		String[] parts = fieldName.split("__");
		if (parts.length == 1) { // no __ found in the string,  default to simple equals match
			criteria.add(property ? Expression.eqProperty(fieldName,  (String)value) : Expression.eq(fieldName,  correctType(fieldName, value)));
			return;
		}
		
		String searchType = parts[parts.length-1];
		String searchField = parts[parts.length-2];
		
		Object[] o = resolveJoins(parts, parts.length-2);
		Class searchClass = (Class)o[0];
		Criteria c = (Criteria)o[1];
		Object searchValue = property ? value : correctType(searchField, value, searchClass);		 		
		Criterion criterion = getCriterion(searchType, searchField, searchValue, property);
		
		if (criterion == null) { // humm no match on the search type..  assume equals
			searchField = searchType;
			// searchType = "eq"; // not really needed
			o = resolveJoins(parts, parts.length-1);
			searchClass = (Class)o[0];
			c = (Criteria)o[1];		
			
			if (property) {
				criterion = Expression.eqProperty(searchField, (String)searchValue);
			} else {
				criterion = Expression.eq(searchField, correctType(searchField, value, searchClass));
			}
		}
		
		c.add(criterion);
	}
	
	public void updateCriteriaFromMap(Map<String, Object> params) throws ManagerException {
		if (params == null) return;
		
		for(Entry<String, Object> entry : params.entrySet()) {
			processField(entry.getKey(), entry.getValue(), false);
		}
	}
	
	public void updateCriteriaFromPropertyMap(Map<String, String> params) throws ManagerException {
		if (params == null) return;
		
		for(Entry<String, String> entry : params.entrySet()) {
			processField(entry.getKey(), entry.getValue(), true);
		}
	}
	
	@SuppressWarnings("unchecked")
	public void updateCriteria(Object params) throws ManagerException {
		
		if (params == null) return;
		
		if (params instanceof Map) {
			updateCriteriaFromMap((Map)params);
			return;			
		}
		
		try {
			Class c = params.getClass();
			java.lang.reflect.Field[] fields = c.getDeclaredFields();
			for (int i=0; i<fields.length; i++) {
				String fieldName = fields[i].getName();
				Object value = PropertyUtils.getProperty(params, fieldName);
				processField(fieldName, value, false);
			}			
		} catch (IllegalAccessException e) {
			log.error(e,e);
		} catch (InvocationTargetException e) {
			log.error(e,e);
		} catch (NoSuchMethodException e) {
			log.error(e,e);
		}	
	}
	
	/**
	 * Returns a the keys Class[] 
	 * @return
	 * @throws DaoException
	 */
	public String getPkProperty() throws ManagerException {
		Configuration cfg = hibernateUtil.getConfiguration();
		PersistentClass pclass = cfg.getClassMapping(modelClass.getName());

		if (pclass == null) {
			throw new ManagerException("Unable to find class : "
					+ modelClass.toString());
		}

		Property componentProperty = pclass.getIdentifierProperty();
		if (componentProperty == null) {
			throw new UnsupportedOperationException("Multiple primary keys not supported");
//			Component component = pclass.getIdentifierMapper();
//			if (component == null) {
//				throw new DaoException("Unable to get pk mapping");
//			}
//			if (log.isDebugEnabled()) log.debug(String.format("Found %d keys for model %s", component.getPropertySpan(), modelClass.getName()));
//
//			Class<?>[] classArray = new Class[component.getColumnSpan()];
//			Iterator<Property> ite = component.getPropertyIterator();
//			int i = 0;
//			while (ite.hasNext()) {
//				Property p = ite.next();
//				if (log.isDebugEnabled()) log.debug("property name: " + p.getName());
//				classArray[i++] = p.getType().getReturnedClass();
//			}
//			return classArray;
		} else {
			return componentProperty.getName();
		}
	}
	
	public void addOrderBy(String orderBy) throws ManagerException {
		boolean desc = orderBy.startsWith("-");
		if (desc) {
			orderBy = orderBy.substring(1);
		}
		
		Criteria c = criteria;
		String[] parts = orderBy.split("__");
		if (parts.length > 1) {
			Object[] o = resolveJoins(parts, parts.length-1);
			// Class searchClass = (Class)o[0];
			c = (Criteria)o[1];
			orderBy = parts[parts.length-1];
		}
		
		
		if (desc) {
			c.addOrder(Order.desc(orderBy));
		} else {
			c.addOrder(Order.asc(orderBy));
		}
	}
}
